﻿' NuGardt API Helper JSON
' Copyright (C) 2013 NuGardt Software
' http://www.nugardt.com
'
' This program is free software: you can redistribute it and/or modify
' it under the terms of the GNU General Public License as published by
' the Free Software Foundation, either version 3 of the License, or
' (at your option) any later version.
'
' This program is distributed in the hope that it will be useful,
' but WITHOUT ANY WARRANTY; without even the implied warranty of
' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
' GNU General Public License for more details.
'
' You should have received a copy of the GNU General Public License
' along with this program.  If not, see <http://www.gnu.org/licenses/>.
'
Imports System.Runtime.Serialization
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Net
Imports System.Runtime.Serialization.Json
Imports NuGardt.API.Helper.JSON.Cache
Imports System.Threading
Imports System.Text

Namespace API.Helper.JSON
  Public Class JsonHelper(Of TBaseResult As IBaseResult)
    Private ReadOnly RequestMethodBuffer As EnumBuffer(Of eRequestMethod, EnumMemberAttribute) = New EnumBuffer(Of eRequestMethod, EnumMemberAttribute)

    Private ReadOnly UseRequestCache As Boolean
    Private ReadOnly Cache As CacheRequest

    Public Sub New(ByVal UseRequestCache As Boolean)
      Me.UseRequestCache = UseRequestCache

      If Me.UseRequestCache Then Me.Cache = New CacheRequest()
    End Sub

#Region "Caching"

    Public Sub ClearCache()
      If Me.UseRequestCache Then Me.Cache.Clear()
    End Sub

    Public Function ReadCache(ByVal Stream As Stream) As Exception
      If Me.UseRequestCache Then
        Return Me.Cache.Read(Stream)
      Else
        Return Nothing
      End If
    End Function

    Public Function WriteCache(ByVal Stream As Stream) As Exception
      If Me.UseRequestCache Then
        Return Me.Cache.Write(Stream)
      Else
        Return Nothing
      End If
    End Function

#End Region

#Region "Function QueryAndParse"

    Public Function QueryAndParse(Of TItem As TBaseResult)(ByVal Method As eRequestMethod,
                                                           ByVal URL As String,
                                                           ByVal RequestData As String,
                                                           Optional ByVal CacheDuration As TimeSpan = Nothing,
                                                           Optional ByVal IgnoreCache As Boolean = Nothing,
                                                           <Out()> Optional ByRef Ex As Exception = Nothing) As TItem
      Ex = Nothing
      Dim Result As TItem = Nothing
      Dim ResponseStream As Stream
      Dim Headers As WebHeaderCollection = Nothing
      Dim Expires As Nullable(Of DateTime)

      Try
        ResponseStream = Me.CacheOrQuery(Method, URL, RequestData, IgnoreCache, Headers, Expires, Ex)
        If (ResponseStream Is Nothing) Then Return Nothing

        Result = Me.ParseResponse(Of TItem)(ResponseStream, Ex)

        'Close stream
        Call ResponseStream.Close()
        Call ResponseStream.Dispose()

        If (Result IsNot Nothing) Then
          Result.CacheExpires = Expires
          If (Headers IsNot Nothing) Then Call Result.ReadHeader(Headers)

          If Me.UseRequestCache Then Call Me.Cache.AddResponse(URL, RequestData, Result.ResponseRaw, CacheDuration)
        End If
      Catch iEx As Exception
        Ex = iEx
      End Try

      Return Result
    End Function

    Public Function QueryAndParse(Of T As {TBaseResult, TArray}, TArrayItem, TArray As ICollection(Of TArrayItem))(ByVal Method As eRequestMethod,
                                                                                                                   ByVal URL As String,
                                                                                                                   ByVal RequestData As String,
                                                                                                                   ByVal CacheDuration As TimeSpan,
                                                                                                                   ByVal IgnoreCache As Boolean,
                                                                                                                   <Out()> ByRef Ex As Exception) As T
      Ex = Nothing
      Dim Result As T = Nothing
      Dim Headers As WebHeaderCollection = Nothing
      Dim ResponseStream As Stream
      Dim Expires As Nullable(Of DateTime)

      Try
        ResponseStream = Me.CacheOrQuery(Method, URL, RequestData, IgnoreCache, Headers, Expires, Ex)
        If (ResponseStream Is Nothing) Then Return Nothing

        Result = Me.ParseResponse(Of T, TArrayItem, TArray)(ResponseStream, Ex)

        'Close stream
        Call ResponseStream.Close()
        Call ResponseStream.Dispose()

        If (Result IsNot Nothing) Then
          If Me.UseRequestCache Then Call Me.Cache.AddResponse(URL, RequestData, Result.ResponseRaw, CacheDuration)
          If (Headers IsNot Nothing) Then Call Result.ReadHeader(Headers)
        End If
      Catch iEx As Exception
        Ex = iEx
      End Try

      Return Result
    End Function

#End Region

#Region "Function ParseResponse"

    Private Overloads Function ParseResponse(Of T As TBaseResult)(ByVal Response As Stream,
                                                                  <Out> ByRef Ex As Exception) As T
      Ex = Nothing
      Dim Serializer As DataContractJsonSerializer
      Dim Result As T = Nothing

      If (Response Is Nothing) Then
        Ex = New ArgumentNullException("Response")
      Else
        Try
          'Create serializer
          Serializer = New DataContractJsonSerializer(GetType(T))

          'Deserialize
          Result = DirectCast(Serializer.ReadObject(Response), T)

          Response.Position = 0
          Result.ResponseRaw = (New StreamReader(Response)).ReadToEnd()
        Catch iEx As Exception
          Ex = iEx
        End Try
      End If

      Return Result
    End Function

    Private Overloads Function ParseResponse(Of T As {TBaseResult, TArray}, TArrayItem, TArray As ICollection(Of TArrayItem))(ByVal Response As Stream,
                                                                                                                              <Out> ByRef Ex As Exception) As T
      Ex = Nothing
      Dim Serializer As DataContractJsonSerializer
      Dim Result As T = Nothing
      Dim ResultArray As TArray

      If (Response Is Nothing) Then
        Ex = New ArgumentNullException("Response")
      Else
        Try
          'Create serializer
          Serializer = New DataContractJsonSerializer(GetType(TArray))

          'Deserialize
          ResultArray = DirectCast(Serializer.ReadObject(Response), TArray)

          'Create instance and add arrray
          'This is a workaround for Mono as the standard method fails with: System.Runtime.Serialization.SerializationException: Deserialization has failed ---> System.InvalidOperationException: Node type Element is not supported in this operation.  (line 1, column 21)
          'Known bug: https://bugzilla.xamarin.com/show_bug.cgi?id=2205
          Result = DirectCast(GetType(T).GetConstructor(New Type() {}).Invoke(New Object() {}), T)
          With DirectCast(Result, TArray)
            Dim dMax As Int32 = ResultArray.Count - 1
            For d As Int32 = 0 To dMax
              Call .Add(ResultArray(d))
            Next d
          End With

          Response.Position = 0
          Result.ResponseRaw = (New StreamReader(Response)).ReadToEnd()
        Catch iEx As Exception
          Ex = iEx
        End Try
      End If

      Return Result
    End Function

#End Region

#Region "Class AsyncStateWithKey"

    Private NotInheritable Class AsyncStateWithKey
      Public ReadOnly Key As Object
      Public ReadOnly Request As WebRequest
      Public ReadOnly RequestString As String
      Public ReadOnly Post As String
      Public ReadOnly CacheDuration As TimeSpan
      Public ReadOnly FromCacheResponseRaw As String
      Public ReadOnly FromCacheExpires As Nullable(Of DateTime)

      Public Sub New(ByVal Key As Object,
                     ByVal State As WebRequest,
                     ByVal Request As String,
                     ByVal Post As String,
                     ByVal CacheDuration As TimeSpan,
                     ByVal FromCacheResponseRaw As String,
                     ByVal FromCacheExpires As Nullable(Of DateTime))
        Me.Key = Key
        Me.Request = State
        Me.RequestString = Request
        Me.Post = Post
        Me.CacheDuration = CacheDuration
        Me.FromCacheResponseRaw = FromCacheResponseRaw
        Me.FromCacheExpires = FromCacheExpires
      End Sub
    End Class

#End Region

#Region "Class StateAsyncResult"

    Private NotInheritable Class StateAsyncResult
      Implements IAsyncResult

      Private ReadOnly m_AsyncState As Object

      Public Sub New(ByVal AsyncState As Object)
        Me.m_AsyncState = AsyncState
      End Sub

      Public ReadOnly Property AsyncState As Object Implements IAsyncResult.AsyncState
        Get
          Return Me.m_AsyncState
        End Get
      End Property

      Public ReadOnly Property AsyncWaitHandle As WaitHandle Implements IAsyncResult.AsyncWaitHandle
        Get
          Return Nothing
        End Get
      End Property

      Public ReadOnly Property CompletedSynchronously As Boolean Implements IAsyncResult.CompletedSynchronously
        Get
          Return True
        End Get
      End Property

      Public ReadOnly Property IsCompleted As Boolean Implements IAsyncResult.IsCompleted
        Get
          Return True
        End Get
      End Property
    End Class

#End Region

#Region "Function QueryAndParseBegin"

    Public Function QueryAndParseBegin(ByVal Key As Object,
                                       ByVal Method As eRequestMethod,
                                       ByVal URL As String,
                                       ByVal RequestData As String,
                                       ByVal Callback As AsyncCallback,
                                       Optional ByVal IgnoreCache As Boolean = False,
                                       Optional ByVal CacheDuration As TimeSpan = Nothing) As IAsyncResult
      Dim Result As IAsyncResult
      Dim Request As WebRequest
      Dim ResponseRaw As String = Nothing
      Dim Expires As Nullable(Of DateTime) = Nothing

      Try
        If (Not IgnoreCache) AndAlso (Me.UseRequestCache) Then ResponseRaw = Me.Cache.GetResponse(URL, RequestData, Expires)

        If (String.IsNullOrEmpty(ResponseRaw)) Then
          Request = HttpWebRequest.Create(URL)
          With Request
            .Method = RequestMethodBuffer.GetValue(Method)
            .ContentType = "application/x-www-form-urlencoded"
          End With

          If (Not String.IsNullOrEmpty(RequestData)) Then
            Dim Writer As New StreamWriter(Request.GetRequestStream())
            Call Writer.Write(RequestData)
            Call Writer.Flush()
          End If

          Result = Request.BeginGetResponse(Callback, New AsyncStateWithKey(Key, Request, URL, RequestData, CacheDuration, ResponseRaw, Expires))
        Else
          Result = Callback.BeginInvoke(New StateAsyncResult(New AsyncStateWithKey(Key, Nothing, URL, RequestData, CacheDuration, ResponseRaw, Expires)), Nothing, Nothing)
        End If
      Catch iEx As Exception
        Result = Callback.BeginInvoke(New StateAsyncResult(iEx), Nothing, Nothing)
      End Try

      Return Result
    End Function

#End Region

#Region "Function QueryAndParseEnd"

    Public Function QueryAndParseEnd(Of T As TBaseResult)(ByVal Result As IAsyncResult,
                                                          <Out()> ByRef Key As Object,
                                                          <Out()> ByRef Response As T) As Exception
      Key = Nothing
      Response = Nothing
      Dim Ex As Exception = Nothing
      Dim State As AsyncStateWithKey
      Dim ResponseStream As Stream
      Dim Headers As WebHeaderCollection = Nothing

      If (Result Is Nothing) Then
        Ex = New ArgumentNullException("Result")
      Else
        If Result.CompletedSynchronously OrElse (Result.AsyncWaitHandle IsNot Nothing) Then Call Result.AsyncWaitHandle.WaitOne()

        State = TryCast(Result.AsyncState, AsyncStateWithKey)

        If (State Is Nothing) Then
          Ex = New Exception("Invalid AsyncState.")
        Else
          Try
            Key = State.Key

            ResponseStream = Me.ResponseEnd(State, Result, Headers, Ex)
            If (ResponseStream Is Nothing) Then Return Ex

            Response = Me.ParseResponse(Of T)(ResponseStream, Ex)

            'Close stream
            Call ResponseStream.Close()
            Call ResponseStream.Dispose()

            With Response
              .CacheExpires = State.FromCacheExpires
              If (Headers IsNot Nothing) Then .ReadHeader(Headers)
            End With

            If Me.UseRequestCache Then Call Me.Cache.AddResponse(State.RequestString, State.Post, Response.ResponseRaw, State.CacheDuration)

            Ex = Nothing
          Catch iEx As Exception
            If (Ex Is Nothing) Then Ex = iEx
          End Try
        End If
      End If

      Return Ex
    End Function

    Public Function QueryAndParseEnd(Of T As {TBaseResult, TArray}, TArrayItem, TArray As ICollection(Of TArrayItem))(ByVal Result As IAsyncResult,
                                                                                                                      <Out()> ByRef Key As Object,
                                                                                                                      <Out()> ByRef Response As T) As Exception
      Key = Nothing
      Response = Nothing
      Dim Ex As Exception = Nothing
      Dim State As AsyncStateWithKey
      Dim ResponseStream As Stream
      Dim Headers As WebHeaderCollection = Nothing

      If (Result Is Nothing) Then
        Ex = New ArgumentNullException("Result")
      Else
        If Result.CompletedSynchronously OrElse (Result.AsyncWaitHandle IsNot Nothing) Then Call Result.AsyncWaitHandle.WaitOne()

        State = TryCast(Result.AsyncState, AsyncStateWithKey)

        If (State Is Nothing) Then
          Ex = New Exception("Invalid AsyncState.")
        Else
          Try
            Key = State.Key

            ResponseStream = Me.ResponseEnd(State, Result, Headers, Ex)
            If (ResponseStream Is Nothing) Then Return Ex

            Response = Me.ParseResponse(Of T, TArrayItem, TArray)(ResponseStream, Ex)

            'Close stream
            Call ResponseStream.Close()
            Call ResponseStream.Dispose()

            With Response
              .CacheExpires = State.FromCacheExpires
              If (Headers IsNot Nothing) Then .ReadHeader(Headers)
            End With

            If Me.UseRequestCache Then Call Me.Cache.AddResponse(State.RequestString, State.Post, Response.ResponseRaw, State.CacheDuration)

            Ex = Nothing
          Catch iEx As Exception
            If (Ex Is Nothing) Then Ex = iEx
          End Try
        End If
      End If

      Return Ex
    End Function

#End Region

#Region "Function CacheOrQuery"

    Private Function CacheOrQuery(ByVal Method As eRequestMethod,
                                  ByVal URL As String,
                                  ByVal RequestData As String,
                                  ByVal IgnoreCache As Boolean,
                                  <Out> ByRef Headers As WebHeaderCollection,
                                  <Out()> ByRef Expires As Nullable(Of DateTime),
                                  <Out> ByRef Ex As Exception) As Stream
      Headers = Nothing
      Expires = Nothing
      Ex = Nothing
      Dim ResponseStream As Stream
      Dim Stream As Stream
      Dim Request As WebRequest
      Dim Response As WebResponse
      Dim ResponseRaw As String = Nothing

      If (Not IgnoreCache) AndAlso (Me.UseRequestCache) Then ResponseRaw = Me.Cache.GetResponse(URL, RequestData, Expires)

      If String.IsNullOrEmpty(ResponseRaw) Then
        Request = HttpWebRequest.Create(URL)
        With Request
          .Method = RequestMethodBuffer.GetValue(Method)
          .ContentType = "application/x-www-form-urlencoded"
        End With

        If (Not String.IsNullOrEmpty(RequestData)) Then
          Dim Writer As New StreamWriter(Request.GetRequestStream())
          Call Writer.Write(RequestData)
          Call Writer.Flush()
        End If

        Try
          Response = Request.GetResponse()
        Catch iEx As WebException
          Ex = iEx
          Response = iEx.Response
        Catch iEx As Exception
          Ex = iEx
          Return Nothing
        End Try

        Headers = Response.Headers
        ResponseStream = Response.GetResponseStream()

        'Copy stream
        Stream = New MemoryStream()
        Call ResponseStream.CopyTo(Stream)
        Stream.Position = 0

        'Close response stream
        Call ResponseStream.Close()
        Call ResponseStream.Dispose()

        'Close response
        Call Response.Close()

        'Replace Stream with MemoryStream so we can seek (read it more than once)
        ResponseStream = Stream
      Else
        ResponseStream = New MemoryStream(Encoding.UTF8.GetBytes(ResponseRaw))
      End If

      Return ResponseStream
    End Function

#End Region

#Region "Function ResponseEnd"

    Private Function ResponseEnd(ByVal State As AsyncStateWithKey,
                                 ByVal Result As IAsyncResult,
                                 <Out()> Headers As WebHeaderCollection,
                                 <Out> ByRef Ex As Exception) As Stream
      Headers = Nothing
      Ex = Nothing
      Dim ResponseStream As Stream = Nothing
      Dim Stream As Stream

      If (Not String.IsNullOrEmpty(State.FromCacheResponseRaw)) Then
        ResponseStream = New MemoryStream(Encoding.UTF8.GetBytes(State.FromCacheResponseRaw))
      ElseIf (State.Request Is Nothing) Then
        Ex = New ArgumentNullException("State.Request")
      Else
        Dim WebResponse As WebResponse

        Try
          WebResponse = State.Request.EndGetResponse(Result)
        Catch iEx As WebException
          Ex = iEx
          WebResponse = iEx.Response
        Catch iEx As Exception
          Ex = iEx
          Return Nothing
        End Try

        ResponseStream = WebResponse.GetResponseStream()
        Headers = WebResponse.Headers

        'Copy stream
        Stream = New MemoryStream()
        Call ResponseStream.CopyTo(Stream)
        Stream.Position = 0

        'Close stream
        Call ResponseStream.Close()
        Call ResponseStream.Dispose()

        'Close response
        Call WebResponse.Close()

        'Replace Stream with MemoryStream so we can seek (read it more than once)
        ResponseStream = Stream
      End If

      Return ResponseStream
    End Function

#End Region
  End Class
End Namespace